﻿(*                  FnSharp              

   Base class library extensions and frameworks aimed 
   specifically at improving F# developers lives!

   Written by: Huw Simpson
   License: Microsoft Public License (Ms-PL) 
   Available at: http://fnsharp.codeplex.com

*)
namespace FnSharp.Bcl.Core

    open System
    open System.Diagnostics
    open System.Text
    open System.Text.RegularExpressions
    open Microsoft.FSharp.Reflection

    [<AutoOpen>]
    module Parse =
        type DateTime with
            /// Tries to convert a string representation of a date and time to a System.DateTime option.
            static member tryParse value =
                value |> DateTime.TryParse |> Option.ofBool2

        type Boolean with
            /// Tries to convert a string representation of a logical value to a System.Boolean option.
            static member tryParse value = 
                value |> Boolean.TryParse |> Option.ofBool2
                
        type Int32 with
            /// Tries to convert a string representation of a number to a System.Int32 option.
            static member tryParse value = 
                value |> Int32.TryParse |> Option.ofBool2

        type UInt32 with
            /// Tries to convert a string representation of a number to a System.UInt32 option.
            static member tryParse value = 
                value |> UInt32.TryParse |> Option.ofBool2

        type Int64 with
            /// Tries to convert a string representation of a number to a System.Int64 option.
            static member tryParse value = 
                value |> Int64.TryParse |> Option.ofBool2

        type UInt64 with
            /// Tries to convert a string representation of a number to a System.UInt64 option.
            static member tryParse value = 
                value |> UInt64.TryParse |> Option.ofBool2

        type Single with
            /// Tries to convert a string representation of a number to a System.Single option.
            static member tryParse value = 
                value |> Single.TryParse |> Option.ofBool2

        type Double with
            /// Tries to convert a string representation of a number to a System.Double option.
            static member tryParse value = 
                value |> Double.TryParse |> Option.ofBool2

        type Decimal with
            /// Tries to convert a string representation of a number to a System.Decimal option.
            static member tryParse value = 
                value |> Decimal.TryParse |> Option.ofBool2

        let private parsers = dict ['b', bool.tryParse >> Option.map (box)
                                    'd', Int32.tryParse >> Option.map (box)
                                    'i', Int32.tryParse >> Option.map (box)
                                    's', box >> Some
                                    'u', UInt32.tryParse >> Option.map (box)
                                    'x', fun s -> s |> String.forall (Char.IsLower) |> Option.ofBool ("0x" + s |> Int32.tryParse) |> Option.map (box)
                                    'X', fun s -> s |> String.forall (Char.IsUpper) |> Option.ofBool ("0x" + s |> Int32.tryParse) |> Option.map (box)
                                    'o', ((+) "0o") >> Int32.tryParse >> Option.map (box)
                                    'e', Double.tryParse >> Option.map (box) // no check for correct format for floats
                                    'E', Double.tryParse >> Option.map (box)
                                    'f', Double.tryParse >> Option.map (box)
                                    'F', Double.tryParse >> Option.map (box)
                                    'g', Double.tryParse >> Option.map (box)
                                    'G', Double.tryParse >> Option.map (box)
                                    'M', Decimal.tryParse >> Option.map (box)
                                    'c', char >> box >> Some ]

        // array of all possible formatters, i.e. [|"%b"; "%d"; ...|]
        let private separators =
            parsers.Keys
            |> Seq.map (fun c -> sprintf "%%%c" c)
            |> Seq.toArray

        type String with
            /// Tries to convert a formatted string to a strong typed representation.
            static member tryParse (pf:PrintfFormat<_,_,_,_,'t>) value =
                // Creates a list of formatter characters from a format string,
                // for example "(%s,%d)" -> ['s', 'd']
                let rec getFormatters xs =
                   match xs with
                   | '%'::'%'::xr -> getFormatters xr
                   | '%'::x::xr -> if parsers.ContainsKey x then x::getFormatters xr
                                   else failwithf "Unknown formatter %%%c" x
                   | x::xr -> getFormatters xr
                   | [] -> []

                let formatStr = pf.Value.Replace("%%", "%")
                let constants = formatStr.Split(separators, StringSplitOptions.None)
                let regex = Regex("^" + String.Join("(.*?)", constants |> Array.map Regex.Escape) + "$")
                let formatters = pf.Value.ToCharArray() // need original string here (possibly with "%%"s)
                                 |> Array.toList |> getFormatters 
                let groups = 
                    regex.Match(value).Groups 
                    |> Seq.cast<Group> 
                    |> Seq.skip 1

                let matches =
                    (groups, formatters)
                    ||> Seq.map2 (fun g f -> g.Value |> parsers.[f])
                    |> Seq.choose (id)
                    |> Seq.toArray

                let retType = typeof<'t>
                let retTypes = retType.GetGenericArguments()
                let paramCountMatch = matches.Length = retTypes.Length
                let paramTypeMatch = (matches,retTypes) ||> Seq.forall2 (fun m t -> m.GetType () = t)
        
                (paramCountMatch && paramTypeMatch) |> Option.ofBoolF (fun _ -> FSharpValue.MakeTuple (matches, retType) :?> 't)

        // active patterns for try-parsing strings
        let (|Date|_|)    = DateTime.tryParse
        let (|Bool|_|)    = Boolean.tryParse
        let (|Int|_|)     = Int32.tryParse
        let (|Int64|_|)   = Int64.tryParse
        let (|UInt|_|)    = UInt32.tryParse
        let (|UInt64|_|)  = UInt64.tryParse
        let (|Single|_|)  = Single.tryParse
        let (|Float|_|)   = Double.tryParse
        let (|Double|_|)  = Double.tryParse
        let (|Decimal|_|) = Decimal.tryParse
        let (|Pattern|_|) = String.tryParse